import asyncore
import threading
import socket
from S3D.Decorators import singleton
import time
from S3D.Net import *
import traceback

class ProxyAttr:
    __slots__ = ("proxy", "name")

    def __init__(self, proxy, name):
        self.proxy = proxy
        self.name  = name

    def __call__(self, *args, **kw):
        data = DataProxyHolder()
        ticket = data.getTicket()

        cmd = DataProxyCommand(self.proxy.clientId)
        cmd.call(ticket, self.name, *args, **kw)

        data.push(cmd)

        return data.waitTicket(ticket)

class Proxy:
    __slots__ = ("clientId", "objectId")
    
    def __init__(self, clientId, objectId):
        self.clientId = clientId
        self.objectId = objectId

    def __getattr__(self, name):
        if name.startswith("_"):
            return object.__getattribute__(self, name)

        return ProxyAttr(self, name)

CMD_REQUEST     = 0
CMD_CALL        = 1
CMD_RESULT      = 2
CMD_RESULT_REF  = 3
CMD_EXCEPTION   = 4

CMD_NAMES       = [p for p in vars() if p.startswith("CMD_")]
CMD_NAMES.sort(key=lambda x: globals()[x])

class DataProxyCommand:
    __slots__   = ("id", "buffer")

    def __init__(self, id):
        self.id     = id
        self.buffer = None

    def request(self, name, ticket):
        p = Packer()
        p.writeShort(CMD_REQUEST)
        p.writeStr(name)
        p.writeInt(ticket)
        self.buffer = p.getValue()

    def result(self, obj, ticket):
        # TODO: check whether it should be result_ref
        p = Packer()
        p.writeShort(CMD_RESULT)
        p.writeInt(ticket)
        p.writeObj(obj)
        self.buffer = p.getValue()

    def result_ref(self, objId, ticket):
        p = Packer()
        p.writeShort(CMD_RESULT_REF)
        p.writeInt(ticket)
        p.writeInt(objId)
        self.buffer = p.getValue()

    def exception(self, exc, ticket):
        # TODO: support exceptions
        self.result(exc, ticket)

    def call(self, ticket, name, *args, **kw):
        p = Packer()
        p.writeShort(CMD_CALL)
        p.writeInt(self.id)
        p.writeInt(ticket)
        p.writeStr(name)
        p.writeObj(args)
        p.writeObj(kw)
        self.buffer = p.getValue()

@singleton
class DataProxyHolder:
    def __init__(self):
        self.clientIndex = 0
        self.clients     = []
        self.objectIndex = 0
        self.objects     = []
        self.ticketIndex = 0
        self.results     = []
        self.commands    = queue.deque([])
        self.lock        = threading.RLock()

    def _findClient(self, id):
        for index, (clientId, client) in enumerate(self.clients):
            if clientId == id:
                return index, client

        return None

    def _findObjIdByName(self, name):
        for id, objName, obj in self.objects:
            if objName == name:
                return id

        return None

    def _findObjById(self, id):
        for objId, objName, obj in self.objects:
            if objId == id:
                return obj

        return None

    def processCommand(self, input, clientId):
        """ parse and execute incoming command """
        with self.lock:
            cmd = input.readShort()
            if cmd == CMD_REQUEST:
                name   = input.readStr()
                ticket = input.readInt()

                objId = self._findObjIdByName(name)

                res = DataProxyCommand(clientId)
                res.result_ref(ticket, objId)
                self.push(res)
                return

            if cmd == CMD_RESULT:
                ticket = input.readInt()
                obj    = input.readObj()

                self.results.append((ticket, obj))
                return

            if cmd == CMD_RESULT_REF:
                ticket = input.readInt()
                objId  = input.readInt()

                self.results.append((ticket, Proxy(clientId, objId)))
                return

            if cmd == CMD_CALL:
                objId   = input.readInt()
                ticket  = input.readInt()  
                name    = input.readStr()
                args    = input.readObj()
                kw      = input.readObj()

                obj     = self._findObjById(objId)

                resCmd = DataProxyCommand(clientId)

                try:
                    res = getattr(obj, name)(*args, **kw)
                    resCmd.result(res, ticket)
                except Exception as e:
                    traceback.print_exc()

                    resCmd.exception(e, ticket)
                    
                self.push(resCmd)
                return

            raise Exception("Unknown command", cmd, CMD_NAMES[cmd])

    def register(self, client):
        """ register client """
        with self.lock:
            id = self.clientIndex
            self.clients.append((id, client))

            self.clientIndex += 1
            return id

    def unregister(self, id):
        """ unregister client """
        with self.lock:
            index, _ = self._findClient(id)
            del self.clients[index]

    def push(self, command):
        """ add command to queue """
        with self.lock:
            res = self._findClient(command.id)
            if res is None:
                raise Exception("Disconnected")

            _, client = res
            client.sendMessage(command)

    def getTicket(self):
        """ get wait ticket """
        with self.lock:
            res = self.ticketIndex
            self.ticketIndex += 1
            return res

    def hasTicketResult(self, ticket):
        """ check whether we got result for specified ticket """
        with self.lock:
            for t, r in self.results:
                if t == ticket:
                    return True

            return False

    def peakTicketResult(self, ticket):
        """ retrieve result for specified ticket """
        with self.lock:
            resInd = -1
            res   = None
            for index, (t, r) in enumerate(self.results):
                if t == ticket:
                    resInd = index
                    res = r
                    break

            assert resInd != -1, "Invalid ticket"
            del self.results[resInd]

            return res

    def registerObj(self, name, obj):
        """ register named object """
        with self.lock:
            id = self.objectIndex
            self.objectIndex += 1

            self.objects.append((id, name, obj))

            return id

    def requestObj(self, name):
        """ request named obj's ref """
        ticket = self.getTicket()

        # TODO: client selection
        clientId, _ = self.clients[0]
        cmd = DataProxyCommand(clientId)
        cmd.request(name, ticket)
        self.push(cmd)

        return self.waitTicket(ticket)

    def waitTicket(self, ticket):
        # TODO: use conditional sync-primitive
        while not self.hasTicketResult(ticket):
            time.sleep(0.002)

        return self.peakTicketResult(ticket)

class DataProxyServer(asyncore.dispatcher):
    def __init__(self, port):
        asyncore.dispatcher.__init__(self)
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.bind(("", port))
        self.listen(1)

    def handle_accept(self):
        pair = self.accept()
        if pair is None:
            return

        conn, addr = pair
        print("Connected:", addr)

        DataProxyClient(sock = conn)

class DataProxyClient(asyncore.dispatcher_with_send):
    def __init__(self, *, addr = None, sock = None):
        super(DataProxyClient, self).__init__(sock=sock)

        if sock is None:
            self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
            self.connect(addr)

        self.data        = DataProxyHolder()
        self.id          = self.data.register(self)
        self.readBuffer  = ReadBuffer()
        self.messageLength = -1

        self.lock        = threading.Lock()
        self.writeBuffer = ReadBuffer()
        self.stopping    = False

    def stop(self):
        with self.lock:
            self.stopping = True

    def writable(self):
        return True

    def handle_write(self):
        with self.lock:
            if self.stopping:
                self.close()
                return

            if self.writeBuffer.getSize() > 0:
                self.send(self.writeBuffer.readAll())

        parent = super(DataProxyClient, self)
        if parent.writable():
            return parent.handle_write()

    def handle_read(self):
        data = self.recv(4096)
        if not data:
            return

        self.readBuffer.append(data)

        input = Unpacker(self.readBuffer)
        
        while True:
            if self.messageLength == -1:
                if self.readBuffer.getSize() < 4:
                    return
                
                self.messageLength = input.readInt()

            if self.readBuffer.getSize() < self.messageLength:
                return

            msgData = self.readBuffer.read(self.messageLength)
            self.messageLength = -1

            try:
                msgInput = Unpacker(io.BytesIO(msgData))
                self.data.processCommand(msgInput, self.id)
            except Exception:
                traceback.print_exc()

    def sendMessage(self, cmd):
        with self.lock:
            self.writeBuffer.append(struct.pack("<i", len(cmd.buffer)))
            self.writeBuffer.append(cmd.buffer)

    def handle_close(self):
        self.data.unregister(self.id)
        self.close()
